Skip to content

pandaproxy: fix unmatched-route 404 body shape#30417

Merged
nguyen-andrew merged 3 commits into
redpanda-data:devfrom
nguyen-andrew:fix-sr-404
May 12, 2026
Merged

pandaproxy: fix unmatched-route 404 body shape#30417
nguyen-andrew merged 3 commits into
redpanda-data:devfrom
nguyen-andrew:fix-sr-404

Conversation

@nguyen-andrew
Copy link
Copy Markdown
Member

@nguyen-andrew nguyen-andrew commented May 7, 2026

When a request hits an HTTP path/method that isn't registered with
pandaproxy::server (REST proxy or schema registry), Redpanda returns
Seastar's built-in 404 body {"message": "Not found", "code": 404}.
Schema-registry clients parse a different envelope (error_code rather
than code), so any client logic that inspects 404 bodies breaks on
this shape.

Real pandaproxy handlers already emit the
{"error_code": <int>, "message": "..."} envelope via
pandaproxy::json::error_body. The fix adds a default handler that
emits the same shape and registers it on Seastar's routes table in
pandaproxy::server::start(). Both REST proxy and schema registry
share pandaproxy::server, so the fix lands on both subsystems with a
single registration site.

The three commits:

  1. pandaproxy: add default_404_handler for unmatched-route 404 body shape
    New default_404_handler class plus C++ unit test exercising the
    body-shape contract in isolation.
  2. pandaproxy: register default_404_handler in server::start()
    Wires the handler so Seastar dispatches to it for any unmatched
    (method, path) combination. Owned via unique_ptr because
    Seastar's add_default_handler does not take ownership (per
    seastar/include/seastar/http/routes.hh:136).
  3. tests/rptest: cover unmatched-route 404 shape on REST proxy and SR
    Ducktape assertions on both ports (8082 and 8081) lock in the wire
    format end-to-end and prove the wiring at the cluster level.

Jira: https://redpandadata.atlassian.net/browse/CORE-16251

Backports Required

  • none - not a bug fix
  • none - this is a backport
  • none - issue does not exist in previous branches
  • none - papercut/not impactful enough to backport
  • v26.1.x
  • v25.3.x
  • v25.2.x

Release Notes

Bug Fixes

  • REST proxy and schema registry now return
    {"error_code": 404, "message": "..."} for unmatched routes,
    matching the envelope clients parse.

Copilot AI review requested due to automatic review settings May 7, 2026 23:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR standardizes the JSON error envelope returned for unmatched routes (unregistered path/method) in Pandaproxy-backed HTTP servers (REST proxy and Schema Registry) by registering a Seastar default handler that emits Pandaproxy’s canonical {"error_code": <int>, "message": "..."} body.

Changes:

  • Add a default_404_handler that returns the Pandaproxy/SR-compatible 404 envelope for unmatched routes.
  • Register the default handler in pandaproxy::server::start() so it applies to both REST proxy and Schema Registry.
  • Add unit + rptest coverage to lock the 404 body shape end-to-end for both ports.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/rptest/tests/schema_registry_test.py Adds an rptest asserting SR unmatched-route 404 returns error_code envelope.
tests/rptest/tests/pandaproxy_test.py Adds an rptest asserting REST proxy unmatched-route 404 returns error_code envelope.
src/v/pandaproxy/test/default_404_handler.cc Adds a C++ unit test validating the default handler’s JSON envelope.
src/v/pandaproxy/test/BUILD Registers the new C++ unit test target.
src/v/pandaproxy/server.h Stores the default handler to ensure it outlives Seastar routes.
src/v/pandaproxy/server.cc Registers the default handler on the Seastar routes table during startup.
src/v/pandaproxy/default_404_handler.h Introduces the default 404 handler implementation.
src/v/pandaproxy/BUILD Exposes the new handler header in the pandaproxy library.

Comment thread src/v/pandaproxy/default_404_handler.h Outdated
Comment thread src/v/pandaproxy/default_404_handler.h Outdated
pgellert
pgellert previously approved these changes May 11, 2026
Copy link
Copy Markdown
Contributor

@pgellert pgellert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find

Comment thread src/v/pandaproxy/default_404_handler.h Outdated
Comment on lines +40 to +51
ss::future<std::unique_ptr<ss::http::reply>> handle(
const ss::sstring&,
std::unique_ptr<ss::http::request>,
std::unique_ptr<ss::http::reply> rep) final {
auto ec = make_error_condition(reply_error_code::not_found);
json::error_body body{.ec = ec, .message = ss::sstring{ec.message()}};
rep->set_status(ss::http::reply::status_type::not_found);
rep->write_body(
_mime_type, pandaproxy::json::rjson_serialize_str(std::move(body)));
return ss::make_ready_future<std::unique_ptr<ss::http::reply>>(
std::move(rep));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it would be nice to move this out of the header file into a .cc implementation file. It's a small file, that's perhaps unlikely to get more implementation, so I think it's a non-blocking nit in this case, but we should for new codem generally prefer to:

  • move as much of the implementation into .cc files
  • prefer 1 bazel target per .cc file unless there's a reason to merge it into an existing target (e.g. cyclical build dependencies)

Comment on lines +262 to 266
_default_handler = std::make_unique<default_404_handler>(
ss::sstring{name(_exceptional_mime_type)});
_server._routes.add_default_handler(_default_handler.get());

_probe = std::make_unique<server_probe>(_ctx, _public_metrics_group_name);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to reset the _default_handler pointer during stop()? Does schema_registry::api::restart() work with this?

I think the answer is yes to both, but just double-checking.

The handler emits the {"error_code": 404, "message": "HTTP 404 Not
Found"} envelope that schema registry and REST proxy clients expect,
rather than Seastar's fallback {"message": "Not found", "code": 404}.
Real pandaproxy handlers already emit this shape via
pandaproxy::json::error_body; this class lets the unmatched-route path
do the same once it is wired into pandaproxy::server in a follow-up
commit.

The handler is added with a unit test before any wiring so its body
contract is exercised in isolation. Wiring lands separately so the
server-side change is visible on its own commit.
Wires the new handler so Seastar's routes table dispatches to it for
any (method, path) combination that no registered route matches.
Without this, Seastar's built-in routes::handle() short-circuits with
its own JSON body that uses {"code": 404} instead of the
{"error_code": 404} shape SR clients expect, breaking 404-fallback
paths in external SR client serializers.

The handler is owned by pandaproxy::server via unique_ptr because
Seastar's add_default_handler does not take ownership (per
seastar/include/seastar/http/routes.hh:136). server::start() creates
it; server::stop() resets it after _server.stop() completes, mirroring
how _probe is torn down and making schema_registry::api::restart()
unambiguously safe across stop->start. The field is also declared
before _server so that on destruction the handler outlives the routes
table.

Both REST proxy and schema registry use pandaproxy::server, so this
fixes the body shape for both subsystems with a single registration.
Asserts that GET on a bogus path returns HTTP 404 with the
{"error_code": 404, "message": "..."} envelope clients expect, rather
than Seastar's fallback {"message": "Not found", "code": 404}. Two
assertions, one per port:

- Schema registry (8081) in schema_registry_test.py
- REST proxy (8082) in pandaproxy_test.py

Both subsystems use pandaproxy::server, so the fix in
pandaproxy::server::start() lands on both. Locking in both wire
formats end-to-end is the test that proves the default_404_handler
registration works for both consumers. Removing the
add_default_handler line would regress the body shape and fail both
assertions in CI.
@vbotbuildovich
Copy link
Copy Markdown
Collaborator

CI test results

test results on build#84300
test_status test_class test_method test_arguments test_kind job_url passed reason test_history
FLAKY(PASS) ShadowLinkingReplicationTests test_with_restart {"storage_mode": "cloud"} integration https://buildkite.com/redpanda/redpanda/builds/84300#019e18d5-1df9-4a45-894a-1887fd3e8f76 10/11 Test PASSES after retries.No significant increase in flaky rate(baseline=0.0265, p0=1.0000, reject_threshold=0.0100. adj_baseline=0.1000, p1=0.3487, trust_threshold=0.5000) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=ShadowLinkingReplicationTests&test_method=test_with_restart

@nguyen-andrew nguyen-andrew merged commit 39c5c64 into redpanda-data:dev May 12, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants